🤖🤖🤖 perf: defer MissingFieldError construction and avoid JSON.stringify in cache diff - #13329
Conversation
…n cache diff Derive diff.complete from the raw missing-field tree instead of eagerly constructing a MissingFieldError (which extends Error and pays V8 stack capture). The error is now built lazily via a getter, only when diff.missing is accessed. Replace the pretty-printed JSON.stringify(objectOrReference, null, 2) in missing-field messages with a cheap __typename lookup, avoiding O(object-size) cost per missing field on embedded parents. Fixes apollographql#13305. Co-authored-by: atlarix-agent <agent@atlarix.dev>
|
@AmariahAK: Thank you for submitting a pull request! Before we can merge it, you'll need to sign the Apollo Contributor License Agreement here: https://contribute.apollographql.com/ |
🦋 Changeset detectedLatest commit: 4358da5 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
✅ AI Style Review — No Changes DetectedNo MDX files were changed in this pull request. Review Log: View detailed log
|
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI (base), Organization UI (inherited) Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthrough
ChangesCache diff diagnostics
Sequence Diagram(s)sequenceDiagram
participant CacheDiff
participant StoreReader
participant SelectionSet
participant DiffResult
CacheDiff->>StoreReader: request diff
StoreReader->>SelectionSet: execute selection set
SelectionSet-->>StoreReader: result and raw missing tree
StoreReader-->>CacheDiff: complete status
CacheDiff->>DiffResult: access missing diagnostics
DiffResult->>StoreReader: construct MissingFieldError lazily
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
hi @apollo-cla i already signed, kindly take a look |
|
Hey @AmariahAK 👋 Appreciate the contribution. A few bits of feedback:
query {
user {
id
name
bestFriends {
id
name
}
}
}where I'm ok not blindly using For data, we have 1 of 3 "types" of objects that can be included in that message:
For normalized objects, I'd like to experiment with replacing the full stringified object with its cache identity (e.g. For non-normalized objects, I think we have to keep For reference objects, I'm ok leaving as-is. Note these proposed solutions are not exhaustive ideas, but rather the things I could think of that maintains debuggability without the need for I think lazy initializing the error probably helps most of the issue, but again, let's see how much we gain/lose by avoiding By the way, in the future, its really helpful if you talk through ideas in the issue beforehand rather than pointing your agent at a PR and implementing verbatim against a suggested fix without our input. It reduces churn and helps both us and you avoid unnecessary implementation/review on something that ultimately might not work. Thanks! PS - I'd kindly ask that you talk to us as a human for additional communication on this PR rather than just having your agent blindly add commits and respond for you. I'd like your personal feedback on how changes here affect both performance and debuggability because I think that needs some personal taste. That also helps me give you a green light on what works best to solve the underlying issue. Thanks! |
|
Oh I also forgot to mention... I'd love if we can test with #13324 as well. I'm doing some work in that PR (which will be released in 4.3) that currently does more with |
|
@jerelmiller apologies, for seeing this late, let me do these changes |
|
No problem, thanks! FYI, I'm working on this branch which is doing a lot of work in Just wanted to note that since I will likely hold off on merging anything here until I get that work done so that I can test it cleanly with anything in this branch. That also means this change will almost certainly land in 4.3 🙂. Just wanted you to be aware so that you can keep an eye on that branch to possibly test against (though its in a pretty broken state right now 😬) |
Use cache.identify() for normalized objects to provide unique, disambiguated cache IDs in missing-field error messages while restoring JSON.stringify for non-normalized (embedded) objects to preserve full debuggability. The lazy MissingFieldError construction (the primary perf fix) is unchanged. Co-authored-by: atlarix-agent <agent@atlarix.dev>
|
@jerelmiller i dont think it conflicts, ive made the changes youve requested, could you take a look? |
Co-authored-by: atlarix-agent <agent@atlarix.dev>
|
Do you have some benchmarks I can review to understand how the different approaches affect performance? For example, is |
|
@jerelmiller Takeaways:
I can add the benchmark to the pr if youd like |
|
Thats helpful thanks! I'm not as worried about the more expensive If I'm looking at this correctly, in the worst case (all objects are non-normalized), we would actually have a slight perf regression because now we're paying the cost of That said, from a taste perspective, I'd love if you could give me some samples of what the new error message looks like with these scenarios (it would help to log the
|
|
@jerelmiller For the error message examples: Scenario 1: All normalized objectsEach Person is disambiguated by cache ID — Scenario 2: All non-normalized (
|
|
Ok awesome, I think that maintains enough information and am happy with that. @wolfie would you mind looking at the comment above to make sure it retains enough information? @AmariahAK let me noodle on whether to include these changes in the upcoming 4.3 release, or as a patch in 4.2.x. I'll talk with the team to see if the change in the missing message is enough to count for a minor release or not. I should have an answer for you soon and will do a full review of the code at that time. Thanks! |
|
@jerelmiller |
Sorry, I've had some busy times so I haven't had the chance to put proper effort into this. However, my issue hasn't really been the clarity of the error messages, but rather that with React, a lot of the queries are re-evaluated as partial during re-rendering causing the construction of heavy error messages that are ultimately thrown away and never shown to the user (since once the state is stable, eventually the queries evaluate properly) So, in short, I'm okay with the error messages as proposed, but I'm not sure I'm a representative opinion, if that makes sense :). |
|
@wolfie no worries! I mostly just wanted to make sure you didn't have any concerns there. Thanks! |
jerelmiller
left a comment
There was a problem hiding this comment.
Appreciate the contribution!
The plan is to get this in the next minor (4.3). I'd love to get this merged sooner than later so that I don't have to burden you with all the merge conflicts once I'm finished with the work on my current branch 🙂.
- Revert unnecessary test reformats in diffAgainstStore.ts and policies.ts
(back to inline JSON.stringify where message format didn't change)
- Remove unnecessary 'as StoreObject' cast in readFromStore.ts
- Use consistent 'object {id}' format for identified-object messages
- Fix falsy fallback to produce 'object {}' instead of 'object'
- Restructure lazy-semantics test to use single toEqual assertion
- Rewrite changeset to focus on performance impact (natural tone)
Co-authored-by: atlarix-agent <agent@atlarix.dev>
|
@jerelmiller |
) This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [actions/stale](https://redirect.github.com/actions/stale) | action | minor | `v10.3.0` → `v10.4.0` | --- > [!WARNING] > Some dependencies could not be looked up. Check the [Dependency Dashboard](..apollographql/issues/11062) for more information. --- ### Release Notes <details> <summary>actions/stale (actions/stale)</summary> ### [`v10.4.0`](https://redirect.github.com/actions/stale/releases/tag/v10.4.0) [Compare Source](https://redirect.github.com/actions/stale/compare/v10.3.0...v10.4.0) #### What's Changed ##### Bug Fix - Fixed `only-issue-types` validation by [@&apollographql#8203;trueberryless](https://redirect.github.com/trueberryless) in [#&apollographql#8203;1338](https://redirect.github.com/actions/stale/pull/1338) ##### Dependency Updates - Bump undici to 6.27.0 via override, clean up stale license files, and version to 10.4.0. by [@&apollographql#8203;dependabot](https://redirect.github.com/dependabot) in [#&apollographql#8203;1342](https://redirect.github.com/actions/stale/pull/1342) #### New Contributors - [@&apollographql#8203;trueberryless](https://redirect.github.com/trueberryless) made their first contribution in [#&apollographql#8203;1338](https://redirect.github.com/actions/stale/pull/1338) **Full Changelog**: <actions/stale@v10.3.0...v10.4.0> </details> --- ### Configuration 📅 **Schedule**: (in timezone America/Los_Angeles) - Branch creation - "every weekend" - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/apollographql/apollo-client). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yNjUuMSIsInVwZGF0ZWRJblZlciI6IjQzLjI2NS4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyI6Y2hyaXN0bWFzX3RyZWU6IGRlcGVuZGVuY2llcyJdfQ==--> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
@jerelmiller i answered your q here |
|
@jerelmiller i answered your q a while back, are there any more changes youd like me to do, or any other areas youd like to focus on? |
|
Apologies, its been a very busy couple days for me. Hoping to have a small gap today to come back to this! |
|
@jerelmiller no worries take your time |
|
Ok finally got my PR in place and have some time. I will look at this first thing in the morning. Thanks for your patience! |
|
Hey, no worries, take your time, thanks for taking time to go through this in depth. |
jerelmiller
left a comment
There was a problem hiding this comment.
Thanks for the contribution!
|
Thanks for the review and the merge |
This PR was opened by the [Changesets release](https://github.com/changesets/action) GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to release-4.3, this PR will be updated.⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ `release-4.3` is currently in **pre mode** so this branch has prereleases rather than normal releases. If you want to exit prereleases, run `changeset pre exit` on `release-4.3`.⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ # Releases ## @apollo/client@4.3.0-alpha.4 ### Patch Changes - [#13347](#13347) [`7d543d6`](7d543d6) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Fix an issue where `network-only` incremental queries could cause cache data to leak into the emitted result when a `@defer` or `@stream` boundary already had complete data in the cache. Cache data inside pending `@defer` objects and `@stream` arrays are now pruned so that only completed `@defer` or `@stream` boundaries are returned. NOTE: This change only applies to `InMemoryCache` when using `GraphQL17Alpha9Handler`. - [#13329](#13329) [`1d581d2`](1d581d2) Thanks [@AmariahAK](https://github.com/AmariahAK)! - Cache diffs for incomplete queries no longer pay the cost of building a full `MissingFieldError` when the `missing` property is not accessed. The error object is now only constructed when the `missing` property is accessed the first time. This improves performance by avoiding a V8 stack capture when `missing` is ignored entirely. As an additional small performance improvement, `JSON.stringify` is no longer used in the error message on objects whose cache ID is known. `JSON.stringify` is only used for non-normalized objects. - [#13347](#13347) [`7d543d6`](7d543d6) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Fix an issue where partial cache data could leak into intermediate incremental results. This could cause runtime crashes if you relied on the presence of values to determine whether the `@defer` data had streamed in or not. Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Fixes #13305.
Problem
Two expensive operations run eagerly on the cache-diff hot path even when the caller only checks
diff.complete(a cheap boolean):JSON.stringify(objectOrReference, null, 2)— pretty-prints the full parent object for every missing-field message, scaling with object size.new MissingFieldError(...)— constructs anErrorsubclass (paying V8 stack capture) per incomplete diff, even thoughdiff.missingis only consumed in__DEV__-guarded logging on most call paths.Solution
JSON.stringifywith a__typenamelookup. Embedded parents now produce"object Profile"instead of the full pretty-printed object dump.diffQueryAgainstStorenow derivesdiff.completefrom the rawexecResult.missingtree (aMissingTree). TheMissingFieldErroris built lazily via a getter, only whendiff.missingis actually accessed. The getter caches the result so repeated access is cheap.All three consumers of
diff.missingare safe — two are__DEV__-guarded, one is a legitimatewatchFragmentconsumer. The hotbroadcastWatchpath only comparesdiff.resultwithequal(), so the getter is never triggered by property enumeration.Changes
src/cache/inmemory/readFromStore.tsMissingFieldErrorgettersrc/cache/inmemory/__tests__/readFromStore.tssrc/cache/inmemory/__tests__/diffAgainstStore.tssrc/cache/inmemory/__tests__/policies.ts.changeset/lazy-diff-diagnostics.mdChecklist:
New feature(not applicable — performance fix for existing behavior)Summary by CodeRabbit
Performance
Bug Fixes